/* * GDevelop JS Platform * Copyright 2013-present Florian Rival (Florian.Rival@gmail.com). All rights reserved. * This project is released under the MIT License. */ /** * A thin wrapper around a Howl object with: * * Extra methods `paused`, `stopped`, `getRate`/`setRate` and `canBeDestroyed` methods. * * Automatic clamping when calling `setRate` to ensure a valid value is passed to Howler.js. * * See https://github.com/goldfire/howler.js#methods for the full documentation. * * @memberof gdjs * @class HowlerSound */ gdjs.HowlerSound = function(o) { Howl.call(this, o); this._paused = false; this._stopped = true; this._canBeDestroyed = false; this._rate = o.rate || 1; //Add custom events listener to keep //track of the sound status. var that = this; this.on('end', function() { if (!that.loop()) { that._canBeDestroyed = true; that._paused = false; that._stopped = true; } }); this.on('playerror', function(id, error) { console.error( "Can't play a sound, considering it as stopped. Error is:", error ); that._paused = false; that._stopped = true; }); // Track play/pause event to be sure the status is // sync'ed with the sound - though this should be redundant // with `play`/`pause` methods already doing that. Keeping // that to be sure that the status is always correct. this.on('play', function() { that._paused = false; that._stopped = false; }); this.on('pause', function() { that._paused = true; that._stopped = false; }); }; gdjs.HowlerSound.prototype = Object.create(Howl.prototype); // Redefine `stop`/`play`/`pause` to ensure the status of the sound // is immediately updated (so that calling `stopped` just after // `play` will return false). gdjs.HowlerSound.prototype.stop = function() { this._paused = false; this._stopped = true; return Howl.prototype.stop.call(this); }; gdjs.HowlerSound.prototype.play = function() { this._paused = false; this._stopped = false; return Howl.prototype.play.call(this); }; gdjs.HowlerSound.prototype.pause = function() { this._paused = true; this._stopped = false; return Howl.prototype.pause.call(this); }; // Add methods to query the status of the sound: gdjs.HowlerSound.prototype.paused = function() { return this._paused; }; gdjs.HowlerSound.prototype.stopped = function() { return this._stopped; }; gdjs.HowlerSound.prototype.canBeDestroyed = function() { return this._canBeDestroyed; }; // Methods to safely update the rate of the sound: gdjs.HowlerSound.prototype.getRate = function() { return this._rate; }; gdjs.HowlerSound.prototype.setRate = function(rate) { this._rate = gdjs.HowlerSoundManager.clampRate(rate); this.rate(this._rate); }; /** * HowlerSoundManager is used to manage the sounds and musics of a RuntimeScene. * * It is basically a container to associate channels to sounds and keep a list * of all sounds being played. * * @memberof gdjs * @class HowlerSoundManager */ gdjs.HowlerSoundManager = function(resources) { this._resources = resources; this._availableResources = {}; //Map storing "audio" resources for faster access. this._globalVolume = 100; this._sounds = {}; this._musics = {}; this._freeSounds = []; //Sounds without an assigned channel. this._freeMusics = []; //Musics without an assigned channel. this._pausedSounds = []; this._paused = false; var that = this; this._checkForPause = function() { if (that._paused) { this.pause(); that._pausedSounds.push(this); } }; document.addEventListener('deviceready', function() { // pause/resume sounds in Cordova when the app is being paused/resumed document.addEventListener( 'pause', function() { var soundList = that._freeSounds.concat(that._freeMusics); for (var key in that._sounds) { if (that._sounds.hasOwnProperty(key)) { soundList.push(that._sounds[key]); } } for (var key in that._musics) { if (that._musics.hasOwnProperty(key)) { soundList.push(that._musics[key]); } } for (var i = 0; i < soundList.length; i++) { var sound = soundList[i]; if (!sound.paused() && !sound.stopped()) { sound.pause(); that._pausedSounds.push(sound); } } that._paused = true; }, false ); document.addEventListener( 'resume', function() { for (var i = 0; i < that._pausedSounds.length; i++) { var sound = that._pausedSounds[i]; if (!sound.stopped()) { sound.play(); } } that._pausedSounds.length = 0; that._paused = false; }, false ); }); }; gdjs.SoundManager = gdjs.HowlerSoundManager; //Register the class to let the engine use it. /** * Ensure rate is in a range valid for Howler.js * @return The clamped rate * @private */ gdjs.HowlerSoundManager.clampRate = function(rate) { if (rate > 4.0) return 4.0; if (rate < 0.5) return 0.5; return rate; }; /** * Return the file associated to the given sound name. * * Names and files are loaded from resources when preloadAudio is called. If no * file is associated to the given name, then the name will be considered as a * filename and will be returned. * * @private * @return The associated filename */ gdjs.HowlerSoundManager.prototype._getFileFromSoundName = function(soundName) { if ( this._availableResources.hasOwnProperty(soundName) && this._availableResources[soundName].file ) { return this._availableResources[soundName].file; } return soundName; }; /** * Store the sound in the specified array, put it at the first index that * is free, or add it at the end if no element is free * ("free" means that the gdjs.HowlerSound can be destroyed). * * @param {Array} arr The array containing the sounds. * @param {gdjs.HowlerSound} arr The gdjs.HowlerSound to add. * @return The gdjs.HowlerSound that have been added (i.e: the second parameter). * @private */ gdjs.HowlerSoundManager.prototype._storeSoundInArray = function(arr, sound) { //Try to recycle an old sound. var index = null; for (var i = 0, len = arr.length; i < len; ++i) { if (arr[i] !== null && arr[i].canBeDestroyed()) { arr[index] = sound; return sound; } } arr.push(sound); return sound; }; gdjs.HowlerSoundManager.prototype.playSound = function( soundName, loop, volume, pitch ) { var soundFile = this._getFileFromSoundName(soundName); var sound = new gdjs.HowlerSound({ src: [soundFile], //TODO: ogg, mp3... loop: loop, volume: volume / 100, rate: gdjs.HowlerSoundManager.clampRate(pitch), }); this._storeSoundInArray(this._freeSounds, sound).play(); sound.on('play', this._checkForPause); }; gdjs.HowlerSoundManager.prototype.playSoundOnChannel = function( soundName, channel, loop, volume, pitch ) { var oldSound = this._sounds[channel]; if (oldSound) { oldSound.unload(); } var soundFile = this._getFileFromSoundName(soundName); var sound = new gdjs.HowlerSound({ src: [soundFile], //TODO: ogg, mp3... loop: loop, volume: volume / 100, rate: gdjs.HowlerSoundManager.clampRate(pitch), }); sound.play(); this._sounds[channel] = sound; sound.on('play', this._checkForPause); }; gdjs.HowlerSoundManager.prototype.getSoundOnChannel = function(channel) { return this._sounds[channel]; }; gdjs.HowlerSoundManager.prototype.playMusic = function( soundName, loop, volume, pitch ) { var soundFile = this._getFileFromSoundName(soundName); var sound = new gdjs.HowlerSound({ src: [soundFile], //TODO: ogg, mp3... loop: loop, html5: true, //Force HTML5 audio so we don't wait for the full file to be loaded on Android. volume: volume / 100, rate: gdjs.HowlerSoundManager.clampRate(pitch), }); this._storeSoundInArray(this._freeMusics, sound).play(); sound.on('play', this._checkForPause); }; gdjs.HowlerSoundManager.prototype.playMusicOnChannel = function( soundName, channel, loop, volume, pitch ) { var oldMusic = this._musics[channel]; if (oldMusic) { oldMusic.unload(); } var soundFile = this._getFileFromSoundName(soundName); var music = new gdjs.HowlerSound({ src: [soundFile], //TODO: ogg, mp3... loop: loop, html5: true, //Force HTML5 audio so we don't wait for the full file to be loaded on Android. volume: volume / 100, rate: gdjs.HowlerSoundManager.clampRate(pitch), }); music.play(); this._musics[channel] = music; music.on('play', this._checkForPause); }; gdjs.HowlerSoundManager.prototype.getMusicOnChannel = function(channel) { return this._musics[channel]; }; gdjs.HowlerSoundManager.prototype.setGlobalVolume = function(volume) { this._globalVolume = volume; if (this._globalVolume > 100) this._globalVolume = 100; if (this._globalVolume < 0) this._globalVolume = 0; Howler.volume(this._globalVolume / 100); }; gdjs.HowlerSoundManager.prototype.getGlobalVolume = function() { return this._globalVolume; }; gdjs.HowlerSoundManager.prototype.clearAll = function() { for (var i = 0; i < this._freeSounds.length; ++i) { if (this._freeSounds[i]) this._freeSounds[i].unload(); } for (var i = 0; i < this._freeMusics.length; ++i) { if (this._freeMusics[i]) this._freeMusics[i].unload(); } this._freeSounds.length = 0; this._freeMusics.length = 0; for (var p in this._sounds) { if (this._sounds.hasOwnProperty(p) && this._sounds[p]) { this._sounds[p].unload(); delete this._sounds[p]; } } for (var p in this._musics) { if (this._musics.hasOwnProperty(p) && this._musics[p]) { this._musics[p].unload(); delete this._musics[p]; } } this._pausedSounds.length = 0; }; gdjs.HowlerSoundManager.prototype.preloadAudio = function( onProgress, onComplete, resources ) { resources = resources || this._resources; //Construct the list of files to be loaded. //For one loaded file, it can have one or more resources //that use it. var files = []; for (var i = 0, len = resources.length; i < len; ++i) { var res = resources[i]; if (res.file && res.kind === 'audio') { this._availableResources[res.name] = res; if (files.indexOf(res.file) === -1) { files.push(res.file); } } } if (files.length === 0) return onComplete(files.length); var loaded = 0; function onLoad(audioFile) { loaded++; if (loaded === files.length) { return onComplete(files.length); } onProgress(loaded, files.length); } var that = this; for (var i = 0; i < files.length; ++i) { (function(audioFile) { var sound = new XMLHttpRequest(); sound.addEventListener('load', onLoad.bind(that, audioFile)); sound.addEventListener('error', onLoad.bind(that, audioFile)); sound.addEventListener('abort', onLoad.bind(that, audioFile)); sound.open('GET', audioFile); sound.send(); })(files[i]); } };